/*
 * Copyright 2011 Ytai Ben-Tsvi. All rights reserved.
 *  
 * 
 * Redistribution and use in source and binary forms, with or without modification, are
 * permitted provided that the following conditions are met:
 * 
 *    1. Redistributions of source code must retain the above copyright notice, this list of
 *       conditions and the following disclaimer.
 * 
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list
 *       of conditions and the following disclaimer in the documentation and/or other materials
 *       provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARSHAN POURSOHI OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * The views and conclusions contained in the software and documentation are those of the
 * authors and should not be interpreted as representing official policies, either expressed
 * or implied.
 */
package ioio.lib.impl;

import ioio.lib.api.exception.ConnectionLostException;
import ioio.lib.impl.IOIOProtocol.IncomingHandler;

import java.util.HashSet;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;

import android.util.Log;

public class IncomingState implements IncomingHandler {
	enum ConnectionState {
		INIT, ESTABLISHED, CONNECTED, DISCONNECTED, UNSUPPORTED_IID
	}

	interface InputPinListener {
		void setValue(int value);
	}

	interface DisconnectListener {
		void disconnected();
	}

	interface DataModuleListener {
		void dataReceived(byte[] data, int size);

		void reportAdditionalBuffer(int bytesRemaining);
	}

	class InputPinState {
		private Queue<InputPinListener> listeners_ = new ConcurrentLinkedQueue<InputPinListener>();
		private boolean currentOpen_ = false;

		void pushListener(InputPinListener listener) {
			listeners_.add(listener);
		}

		void closeCurrentListener() {
			if (currentOpen_) {
				currentOpen_ = false;
				listeners_.remove();
			}
		}

		void openNextListener() {
			assert (!listeners_.isEmpty());
			if (!currentOpen_) {
				currentOpen_ = true;
			}
		}

		void setValue(int v) {
			assert (currentOpen_);
			listeners_.peek().setValue(v);
		}
	}

	class DataModuleState {
		private Queue<DataModuleListener> listeners_ = new ConcurrentLinkedQueue<IncomingState.DataModuleListener>();
		private boolean currentOpen_ = false;

		void pushListener(DataModuleListener listener) {
			listeners_.add(listener);
		}

		void closeCurrentListener() {
			if (currentOpen_) {
				currentOpen_ = false;
				listeners_.remove();
			}
		}

		void openNextListener() {
			assert (!listeners_.isEmpty());
			if (!currentOpen_) {
				currentOpen_ = true;
			}
		}

		void dataReceived(byte[] data, int size) {
			assert (currentOpen_);
			listeners_.peek().dataReceived(data, size);
		}

		public void reportAdditionalBuffer(int bytesRemaining) {
			assert (currentOpen_);
			listeners_.peek().reportAdditionalBuffer(bytesRemaining);
		}
	}

	private final InputPinState[] intputPinStates_ = new InputPinState[Constants.NUM_PINS];
	private final DataModuleState[] uartStates_ = new DataModuleState[Constants.NUM_UART_MODULES];
	private final DataModuleState[] twiStates_ = new DataModuleState[Constants.NUM_TWI_MODULES];
	private final DataModuleState[] spiStates_ = new DataModuleState[Constants.NUM_SPI_MODULES];
	private final Set<DisconnectListener> disconnectListeners_ = new HashSet<IncomingState.DisconnectListener>();
	private ConnectionState connection_ = ConnectionState.INIT;
	public String hardwareId_;
	public String bootloaderId_;
	public String firmwareId_;

	public IncomingState() {
		for (int i = 0; i < intputPinStates_.length; ++i) {
			intputPinStates_[i] = new InputPinState();
		}
		for (int i = 0; i < uartStates_.length; ++i) {
			uartStates_[i] = new DataModuleState();
		}
		for (int i = 0; i < twiStates_.length; ++i) {
			twiStates_[i] = new DataModuleState();
		}
		for (int i = 0; i < spiStates_.length; ++i) {
			spiStates_[i] = new DataModuleState();
		}
	}

	synchronized public void waitConnectionEstablished() throws InterruptedException,
			ConnectionLostException {
		while (connection_ == ConnectionState.INIT) {
			wait();
		}
		if (connection_ == ConnectionState.DISCONNECTED) {
			throw new ConnectionLostException();
		}
	}

	synchronized public boolean waitForInterfaceSupport()
			throws InterruptedException, ConnectionLostException {
		if (connection_ == ConnectionState.INIT) {
			throw new IllegalStateException(
					"Have to connect before waiting for interface support");
		}
		while (connection_ == ConnectionState.ESTABLISHED) {
			wait();
		}
		if (connection_ == ConnectionState.DISCONNECTED) {
			throw new ConnectionLostException();
		}
		return connection_ == ConnectionState.CONNECTED;
	}

	synchronized public void waitDisconnect() throws InterruptedException {
		while (connection_ != ConnectionState.DISCONNECTED) {
			wait();
		}
	}

	public void addInputPinListener(int pin, InputPinListener listener) {
		intputPinStates_[pin].pushListener(listener);
	}

	public void addUartListener(int uartNum, DataModuleListener listener) {
		uartStates_[uartNum].pushListener(listener);
	}

	public void addTwiListener(int twiNum, DataModuleListener listener) {
		twiStates_[twiNum].pushListener(listener);
	}

	public void addSpiListener(int spiNum, DataModuleListener listener) {
		spiStates_[spiNum].pushListener(listener);
	}

	synchronized public void addDisconnectListener(DisconnectListener listener)
			throws ConnectionLostException {
		checkNotDisconnected();
		disconnectListeners_.add(listener);
	}

	synchronized public void removeDisconnectListener(
			DisconnectListener listener) {
		if (connection_ != ConnectionState.DISCONNECTED) {
			disconnectListeners_.remove(listener);
		}
	}

	@Override
	public void handleConnectionLost() {
		logMethod("handleConnectionLost");
		synchronized (this) {
			connection_ = ConnectionState.DISCONNECTED;
		}
		for (DisconnectListener listener : disconnectListeners_) {
			listener.disconnected();
		}
		disconnectListeners_.clear();
		synchronized (this) {
			notifyAll();
		}
	}

	@Override
	public void handleSoftReset() {
		logMethod("handleSoftReset");
		for (InputPinState pinState : intputPinStates_) {
			pinState.closeCurrentListener();
		}
		for (DataModuleState uartState : uartStates_) {
			uartState.closeCurrentListener();
		}
		for (DataModuleState twiState : twiStates_) {
			twiState.closeCurrentListener();
		}
		for (DataModuleState spiState : spiStates_) {
			spiState.closeCurrentListener();
		}
	}

	@Override
	synchronized public void handleCheckInterfaceResponse(boolean supported) {
		logMethod("handleCheckInterfaceResponse", supported);
		connection_ = supported ? ConnectionState.CONNECTED
				: ConnectionState.UNSUPPORTED_IID;
		notifyAll();
	}

	@Override
	public void handleSetChangeNotify(int pin, boolean changeNotify) {
		logMethod("handleSetChangeNotify", pin, changeNotify);
		if (changeNotify) {
			intputPinStates_[pin].openNextListener();
		} else {
			intputPinStates_[pin].closeCurrentListener();
		}
	}

	@Override
	public void handleRegisterPeriodicDigitalSampling(int pin, int freqScale) {
		logMethod("handleRegisterPeriodicDigitalSampling", pin, freqScale);
		assert (false);
	}

	@Override
	public void handleAnalogPinStatus(int pin, boolean open) {
		logMethod("handleAnalogPinStatus", pin, open);
		if (open) {
			intputPinStates_[pin].openNextListener();
		} else {
			intputPinStates_[pin].closeCurrentListener();
		}
	}

	@Override
	public void handleUartData(int uartNum, int numBytes, byte[] data) {
		// logMethod("handleUartData", uartNum, numBytes, data);
		uartStates_[uartNum].dataReceived(data, numBytes);
	}

	@Override
	public void handleUartOpen(int uartNum) {
		logMethod("handleUartOpen", uartNum);
		uartStates_[uartNum].openNextListener();
	}

	@Override
	public void handleUartClose(int uartNum) {
		logMethod("handleUartClose", uartNum);
		uartStates_[uartNum].closeCurrentListener();
	}

	@Override
	public void handleSpiOpen(int spiNum) {
		logMethod("handleSpiOpen", spiNum);
		spiStates_[spiNum].openNextListener();
	}

	@Override
	public void handleSpiClose(int spiNum) {
		logMethod("handleSpiClose", spiNum);
		spiStates_[spiNum].closeCurrentListener();
	}

	@Override
	public void handleI2cOpen(int i2cNum) {
		logMethod("handleI2cConfigureMaster", i2cNum);
		twiStates_[i2cNum].openNextListener();
	}

	@Override
	public void handleI2cClose(int i2cNum) {
		logMethod("handleI2cClose", i2cNum);
		twiStates_[i2cNum].closeCurrentListener();
	}


	@Override
	public void handleEstablishConnection(byte[] hardwareId,
			byte[] bootloaderId, byte[] firmwareId) {
		hardwareId_ = new String(hardwareId);
		bootloaderId_ = new String(bootloaderId);
		firmwareId_ = new String(firmwareId);

		Log.i("IncomingState", "IOIO Connection established. Hardware ID: "
				+ hardwareId_ + " Bootloader ID: "
				+ bootloaderId_ + " Firmware ID: "
				+ firmwareId_);
		synchronized (this) {
			connection_ = ConnectionState.ESTABLISHED;
			notifyAll();
		}
	}

	@Override
	public void handleUartReportTxStatus(int uartNum, int bytesRemaining) {
		// logMethod("handleUartReportTxStatus", uartNum, bytesRemaining);
		uartStates_[uartNum].reportAdditionalBuffer(bytesRemaining);
	}

	@Override
	public void handleI2cReportTxStatus(int i2cNum, int bytesRemaining) {
		// logMethod("handleI2cReportTxStatus", i2cNum, bytesRemaining);
		twiStates_[i2cNum].reportAdditionalBuffer(bytesRemaining);
	}

	@Override
	public void handleSpiData(int spiNum, int ssPin, byte[] data, int dataBytes) {
		// logMethod("handleSpiData", spiNum, ssPin, data, dataBytes);
		spiStates_[spiNum].dataReceived(data, dataBytes);
	}

	@Override
	public void handleReportDigitalInStatus(int pin, boolean level) {
		// logMethod("handleReportDigitalInStatus", pin, level);
		intputPinStates_[pin].setValue(level ? 1 : 0);
	}

	@Override
	public void handleReportPeriodicDigitalInStatus(int frameNum,
			boolean[] values) {
		logMethod("handleReportPeriodicDigitalInStatus", frameNum, values);
	}

	@Override
	public void handleReportAnalogInStatus(int pins[], int values[]) {
		// logMethod("handleReportAnalogInStatus", pins, values);
		for (int i = 0; i < pins.length; ++i) {
			intputPinStates_[pins[i]].setValue(values[i]);
		}
	}

	@Override
	public void handleSpiReportTxStatus(int spiNum, int bytesRemaining) {
		// logMethod("handleSpiReportTxStatus", spiNum, bytesRemaining);
		spiStates_[spiNum].reportAdditionalBuffer(bytesRemaining);
	}

	@Override
	public void handleI2cResult(int i2cNum, int size, byte[] data) {
		logMethod("handleI2cResult", i2cNum, size, data);
		twiStates_[i2cNum].dataReceived(data, size);
	}

	private void checkNotDisconnected() throws ConnectionLostException {
		if (connection_ == ConnectionState.DISCONNECTED) {
			throw new ConnectionLostException();
		}
	}

	private void logMethod(String name, Object... args) {
		StringBuffer msg = new StringBuffer(name);
		msg.append('(');
		for (int i = 0; i < args.length; ++i) {
			if (i != 0) {
				msg.append(", ");
			}
			msg.append(args[i]);
		}
		msg.append(')');

		Log.v("IncomingState", msg.toString());
	}
}
